const mongoose = require('mongoose');
const tests = require('./tests');
const functions = require('./functions');
const files = require('./files');
const utility = require('./utility');
const BreakPoint = require('./mongo/break-point');
const PhoenixError = require('./phoenix-error');
const projectVersions = require('./project-versions');
const invokeRelations = require('./invoke-relations');

const variableKeys = ['params', 'global', 'statics', 'malloc', 'local'];

function getJson(breakPoint) {
  breakPoint = breakPoint.toObject();
  delete breakPoint.expandedVariables;
  variableKeys.forEach((key) => {
    if (utility.isEmpty(breakPoint[key])) {
      delete breakPoint[key];
    }
  });
  return breakPoint;
}

async function getBreakPoint(breakPointId) {
  return BreakPoint.findById(mongoose.Types.ObjectId(breakPointId));
}

function recurseGenerate(result, type, types) {
  result['@type'] = type;
  if (!type || !types[type]) {
    return;
  }
  const originalType = types[type];
  const canonicalType = types[originalType['@canonical']];
  if (!canonicalType) {
    return;
  }
  const unqualifiedType = types[canonicalType['@unqualified']];
  if (!unqualifiedType) {
    return;
  }
  const attributes = unqualifiedType['@attributes'];
  if (!attributes) {
    return;
  }
  const attributeSet = new Set(attributes);
  if (attributeSet.has('isRecord') || attributeSet.has('isCXXRecord')) {
    if (!unqualifiedType['@fields']) {
      return;
    }
    result['@value'] = {};
    Object.keys(unqualifiedType['@fields']).forEach((field) => {
      result['@value'][field] = {};
      recurseGenerate(result['@value'][field], unqualifiedType['@fields'][field]['@type'], types);
    });
    // 处理bases多重继承
    if (unqualifiedType['@bases']) {
      result['@value']['@bases'] = {};
      unqualifiedType['@bases'].forEach((base) => {
        const baseTypeName = base['@type'];
        result['@value']['@bases'][baseTypeName] = {};
        recurseGenerate(result['@value']['@bases'][baseTypeName], baseTypeName, types);
      });
    }
  } else if (attributeSet.has('isArray')) {
    result['@elementType'] = originalType['@elementType'];
    result['@length'] = originalType['@length'];
  }
}

function generateExpandedData(key, variable, types) {
  const result = {};
  if (!variable || !variable['@type']) {
    return result;
  }
  result['@type'] = variable['@type'];
  result['@name'] = variable['@name'] || key;
  if (variable['@elementType']) {
    result['@elementType'] = variable['@elementType'];
    result['@length'] = variable['@length'];
    return result;
  }
  recurseGenerate(result, variable['@type'], types);
  return result;
}

async function updateExpandedVars(breakPoint, functionVariable, types) {
  const update = { $set: {}, $unset: {} };
  if (!utility.isEmpty(breakPoint.expandedVariables)) {
    variableKeys.forEach((variableKey) => {
      if (!breakPoint.expandedVariables[variableKey]) {
        return;
      }
      Object.keys(breakPoint.expandedVariables[variableKey]).forEach((key) => {
        let functionVariableVars;
        if (variableKey === 'malloc') {
          functionVariableVars = functionVariable
            && functionVariable[variableKey]
            && functionVariable[variableKey][key] ? functionVariable[variableKey][key] : null;
        } else if (variableKey === 'local') {
          const [localName, type] = key.split('|');
          functionVariableVars = functionVariable
            && functionVariable.variables
            && functionVariable.variables[variableKey]
            && functionVariable.variables[variableKey][localName] ? functionVariable.variables[variableKey][localName].find((item) => item['@type'] === type) : null;
        } else {
          functionVariableVars = functionVariable
            && functionVariable.variables
            && functionVariable.variables[variableKey]
            && functionVariable.variables[variableKey][key] ? functionVariable.variables[variableKey][key] : null;
        }
        if (
          (
            !breakPoint
            || !breakPoint[variableKey]
            || breakPoint[variableKey].indexOf(key) < 0)
          || (
            !functionVariableVars
          )
        ) {
          update.$unset[`expandedVariables.${variableKey}.${key}`] = 1;
          // 类型不一致，需要重新组数据
        } else if (breakPoint.expandedVariables[variableKey][key]['@type'] !== functionVariableVars['@type']) {
          update.$set[`expandedVariables.${variableKey}.${key}`] = generateExpandedData(key, functionVariableVars, types);
        }
      });
    });
  }
  // 补充变量展开数据
  variableKeys.forEach((variableKey) => {
    if (!breakPoint[variableKey]) {
      return;
    }
    breakPoint[variableKey].forEach((key) => {
      let functionVariableVars = null;
      if (variableKey === 'malloc') {
        functionVariableVars = functionVariable
          && functionVariable[variableKey]
          && functionVariable[variableKey][key] ? functionVariable[variableKey][key] : null;
      } else if (variableKey === 'local') {
        const [name, type] = key.split('|');
        if (functionVariable
          && functionVariable.variables
          && functionVariable.variables[variableKey]
          && functionVariable.variables[variableKey][name]) {
          functionVariable.variables[variableKey][name].forEach((localVar) => {
            if (localVar['@type'] === type) {
              localVar['@name'] = name;
              functionVariableVars = localVar;
            }
          });
        }
      } else {
        functionVariableVars = functionVariable
          && functionVariable.variables
          && functionVariable.variables[variableKey]
          && functionVariable.variables[variableKey][key] ? functionVariable.variables[variableKey][key] : null;
      }
      // variable不存在忽略
      if (!functionVariableVars) {
        return;
      }
      // expandedVariables里不存在添加
      if (!breakPoint.expandedVariables
        || !breakPoint.expandedVariables[variableKey]
        || !breakPoint.expandedVariables[variableKey][key]) {
        update.$set[`expandedVariables.${variableKey}.${key}`] = generateExpandedData(key, functionVariableVars, types);
      }
    });
  });

  if (!utility.isEmpty(update.$set) || !utility.isEmpty(update.$unset)) {
    breakPoint = BreakPoint.findOneAndUpdate({ _id: mongoose.Types.ObjectId(breakPoint._id) }, update, { new: true });
  }
  return breakPoint;
}

async function getBreakPointsByTestcase(testcaseId, {
  functionVariable = null,
  types = null,
  breakPointId = null,
  fileTypesMap = new Map(),
  functionVariablesMap = new Map(),
} = {}) {
  let breakPoints = await BreakPoint.aggregate([
    {
      $match: {
        testcaseId: mongoose.Types.ObjectId(testcaseId),
      },
    },
    {
      $lookup: {
        from: 'files',
        localField: 'fileId',
        foreignField: '_id',
        as: 'break_files',
      },
    },
    {
      $unwind: '$break_files',
    },
    {
      $lookup: {
        from: 'functions',
        localField: 'functionId',
        foreignField: '_id',
        as: 'break_files_funcs',
      },
    },
    {
      $unwind: '$break_files_funcs',
    },
    {
      $project: {
        line: 1,
        times: 1,
        exit: 1,
        number: 1,
        params: 1,
        global: 1,
        statics: 1,
        malloc: 1,
        local: 1,
        fileId: 1,
        filePath: '$break_files.path',
        functionId: 1,
        functionName: '$break_files_funcs.functionName',
        expandedVariables: 1,
        code: 1,
      },
    },
  ]);
  if (utility.isEmpty(breakPoints)) {
    return [];
  }
  let breakPointIndex = 0;
  breakPoints = breakPoints.filter((p) => {
    breakPointIndex += 1;
    p.number = breakPointIndex;
    if (breakPointId && breakPointId.toString() !== p._id.toString()) {
      return false;
    }
    return true;
  });
  if (functionVariable) {
    functionVariablesMap.set(functionVariable.functionId.toString(), functionVariable);
  }
  if (types) {
    fileTypesMap.set(types.fileId.toString(), types.types);
  }
  return utility.arrayChunkOperation(breakPoints, 1, async (breakPoint) => {
    const fileId = breakPoint.fileId.toString();
    const functionId = breakPoint.functionId.toString();
    if (!fileTypesMap.has(fileId)) {
      const fileTypes = await files.getTypes(fileId);
      fileTypesMap.set(fileId, fileTypes.types);
    }
    if (!functionVariablesMap.has(functionId)) {
      const functionVariables = await functions.getFunctionVariables(functionId);
      functionVariablesMap.set(functionId, functionVariables);
    }
    const { expandedVariables } = await updateExpandedVars(breakPoint, functionVariablesMap.get(functionId), fileTypesMap.get(fileId));
    return {
      breakPointId: breakPoint._id,
      line: breakPoint.line,
      times: breakPoint.times,
      exit: breakPoint.exit,
      number: breakPoint.number,
      fileId: breakPoint.fileId,
      filePath: breakPoint.filePath,
      functionId: breakPoint.functionId,
      entryFunctionId: breakPoint.entryFunctionId,
      functionName: breakPoint.functionName,
      expandedVariables,
      code: breakPoint.code,
    };
  });
}
async function upsertBreakPoint(breakPointId, data, versionType) {
  const insert = !breakPointId;
  let breakPoint;
  if (insert) {
    const breakPointModel = new BreakPoint(data);
    breakPoint = await breakPointModel.save();
  } else {
    const update = { $set: {} };
    ['line', 'times', 'exit', 'fileId', 'functionId', 'code', 'entryFunctionId', ...variableKeys].forEach((key) => {
      if (data[key] !== undefined) {
        update.$set[key] = data[key];
      }
    });
    breakPoint = await BreakPoint.findOneAndUpdate({ _id: mongoose.Types.ObjectId(breakPointId) }, update, { new: true });
  }
  const { entryFunctionId } = data;
  await tests.removeResultOfExecution(breakPoint.testcaseId);
  if (versionType === projectVersions.versionType.integrationTest) {
    await functions.updateFunctionInvokeCoverage(entryFunctionId);
  }
  await functions.updateFunctionCoverage(entryFunctionId);
  return breakPoint;
}

async function deleteBreakPoint(filter) {
  return BreakPoint.deleteMany(filter);
}
/**
 * 根据文件行返回函数变量信息
 * @param {String|ObjectId} functionId  入口函数
 * @param {String|ObjectId} fileId  文件id
 * @param {String} versionType  版本类型
 * @param {Number} line 文件中行
 * @returns
 */
async function getObservableVariables(functionId, fileId, line, versionType) {
  const invokeFunc = await functions.getFunctionByFileLine(fileId, line);
  const includesFileIds = [fileId];
  const result = {
    fileId,
    files: [],
  };
  if (versionType === projectVersions.versionType.integrationTest) {
    const invokeFuncs = await invokeRelations.retrieveRelations(functionId, { initRelations: true });
    invokeFuncs.forEach((func) => {
      if (func.toFuncFileId) {
        includesFileIds.push(func.toFuncFileId);
      }
    });
  }
  result.files = (await files.getFilesByIds(includesFileIds, { projection: ['_id', 'path'] }))
    .map((file) => ({ fileId: file._id, filePath: file.path }));
  // 寻找代码行中的函数
  if (!invokeFunc) {
    return result;
  }
  result.functionId = invokeFunc._id;
  const functionVariable = await functions.getFunctionVariables(invokeFunc._id);
  ['params', 'global', 'statics', 'local'].forEach((variableKey) => {
    if (functionVariable.variables && functionVariable.variables[variableKey]) {
      result[variableKey] = [];
      Object.keys(functionVariable.variables[variableKey]).forEach((key) => {
        if (variableKey === 'local') {
          if (!utility.isArrayType(functionVariable.variables[variableKey][key])) {
            return;
          }
          const uniqueSet = new Set();
          functionVariable.variables[variableKey][key].forEach((localVariable) => {
            const uniqueKey = `${key}|${localVariable['@type']}`;
            if (uniqueSet.has(uniqueKey)) {
              return;
            }
            uniqueSet.add(uniqueKey);
            result[variableKey].push({
              key: uniqueKey,
              label: key,
              type: localVariable['@type'],
            });
          });
          return;
        }
        result[variableKey].push({
          key,
          label: variableKey === 'statics' ? functionVariable.variables[variableKey][key]['@name'] : key,
          type: functionVariable.variables[variableKey][key]['@type'],
        });
      });
    }
  });
  if (functionVariable.malloc) {
    Object.keys(functionVariable.malloc).forEach((key) => {
      if (result.malloc === undefined) {
        result.malloc = [];
      }
      result.malloc.push({
        key,
        label: key,
        type: functionVariable.malloc[key]['@type'],
      });
    });
  }
  return result;
}
/**
 * 断点数组变量下标
 * @param {String|Object} breakPointId 断点id
 * @param {String} keyPath  变量fieldPath
 * @param {String} value  变量值
 */
async function updateBreakPointArrayIndexRange(breakPointId, keyPath, val) {
  // val格式校验
  const reg = /^(\d+,)*\d+$/;
  const reg1 = /^\d+-\d+$/;
  let indexes = [];
  if (reg.test(val)) {
    indexes = val.split(',').map((val) => parseInt(val, 10));
    if (indexes.length > 100) {
      throw new PhoenixError('invalid request', 'index range must be less than 100');
    }
    // 判断是否有重复
    if (new Set(indexes).size < indexes.length) {
      throw new PhoenixError('invalid request', 'index duplicated');
    }
  } else if (reg1.test(val)) {
    const [start, end] = val.split('-').map((val) => parseInt(val, 10));
    if (start >= end) {
      throw new PhoenixError('invalid request', 'range start should be less than end');
    }
    if (end - start > 99) {
      throw new PhoenixError('invalid request', 'index range must be less than 100');
    }
    for (let index = start; index <= end; index += 1) {
      indexes.push(index);
    }
  } else {
    throw new PhoenixError('invalid request', 'malformed value');
  }
  const [breakpointElementType] = await BreakPoint.aggregate([
    {
      $match: {
        _id: mongoose.Types.ObjectId(breakPointId),
        [`expandedVariables.${keyPath}`]: { $exists: 1 },
      },
    },
    {
      $project: {
        elementType: `$expandedVariables.${keyPath}.@elementType`,
        fileId: 1,
      },
    },
  ]);
  if (!breakpointElementType) {
    throw new PhoenixError('not found', 'path not exists');
  }
  const elementTypeName = breakpointElementType.elementType;
  const fileTypes = await files.getTypes(breakpointElementType.fileId);
  const elementTypeExpanded = {};
  recurseGenerate(elementTypeExpanded, elementTypeName, fileTypes.types);
  const { n } = await BreakPoint.updateOne({
    _id: breakPointId,
  }, [
    {
      // 旧的value字段备份
      $set: {
        [`expandedVariables.${keyPath}.@value__bak`]: `$expandedVariables.${keyPath}.@value`,
      },
    },
    {
      // 删除旧的value字段
      $unset: [`expandedVariables.${keyPath}.@value`],
    },
    {
      $set: {
        [`expandedVariables.${keyPath}.__indexRange`]: val,
        // 按照新的数组下标，从旧的value中取出对应的值(如果不存在则取出undefined)，保存到新的value字段
        [`expandedVariables.${keyPath}.@value`]: indexes.reduce((obj, idx) => {
          const k = idx;
          const v = {
            ...elementTypeExpanded,
            '@value': `$expandedVariables.${keyPath}.@value__bak.${idx}.@value`,
            '@type': `$expandedVariables.${keyPath}.@elementType`,
          };
          obj[k] = v;
          return obj;
        }, {}),
      },
    },
    {
      // 删除备份value字段
      $unset: [`expandedVariables.${keyPath}.@value__bak`],
    },
  ]);
  if (n === 0) {
    throw new PhoenixError('not found', 'break point or path not exists');
  }
}
/**
 *  添加断点指针目标
 * @param {String|ObjectId} breakPointId 断点id
 * @param {String} pointerTargetName  指针目标名称
 * @param {String} typeName 类型名称
 * @param {String} elementTypeName 数组子类型名称
 * @param {Number} length 数组长度
 */
async function addBreakPointPointTargets(breakPointId, pointerTargetName, typeName, elementTypeName, length) {
  const breakPointData = await getBreakPoint(breakPointId);
  if (breakPointData.expandedVariables
    && breakPointData.expandedVariables.pointerTargets
    && breakPointData.expandedVariables.pointerTargets[pointerTargetName]) {
    throw new PhoenixError('conflict', `break point pointerTargetName:${pointerTargetName} is exists`);
  }
  const doc = { $set: {} };
  if (elementTypeName) {
    typeName = functions.getArrayTypeName(elementTypeName, length);
    doc.$set[`expandedVariables.pointerTargets.${pointerTargetName}`] = {
      '@type': typeName || undefined,
      '@elementType': elementTypeName || undefined,
      '@length': length ? Number.parseInt(length.toString(), 10) : undefined,
    };
  } else {
    const elementTypeExpanded = {};
    const fileTypes = await files.getTypes(breakPointData.fileId);
    recurseGenerate(elementTypeExpanded, typeName, fileTypes.types);
    doc.$set[`expandedVariables.pointerTargets.${pointerTargetName}`] = elementTypeExpanded;
  }
  return BreakPoint.updateOne({ _id: mongoose.Types.ObjectId(breakPointId) }, doc);
}

async function updateBreakPointOutputResultByTestcase(testcaseId, runtime) {
  const ps = [];
  const testcase = await tests.getTest(testcaseId);
  const breakpoints = await BreakPoint.find({ testcaseId }).lean();
  const filePathToIdMap = new Map();
  const filePaths = new Set();
  filePaths.add(testcase.filepath);
  Object.keys(runtime).forEach((lineKey) => {
    const [, filePath] = lineKey.split('@');
    if (filePath) {
      filePaths.add(filePath);
    }
  });
  (await Promise.all(Array.from(filePaths)
    .map((filePath) => files.getFileByPath(testcase.versionId, filePath))))
    .forEach((file) => {
      filePathToIdMap.set(file.path, file._id.toString());
    });
  Object.keys(runtime).forEach((lineKey) => {
    const [line, filePath] = lineKey.split('@');
    if (filePath && !filePathToIdMap.has(filePath)) {
      return;
    }
    const fileId = filePathToIdMap.get(filePath) || testcase.fileId.toString();
    Object.keys(runtime[lineKey]).forEach((timeKey) => {
      const times = parseInt(timeKey, 10) + 1; // 核心time从0开始
      const breakpointExists = breakpoints.find((item) => item.line === parseInt(line, 10)
        && item.times === times
        && item.fileId.toString() === fileId);
      if (!breakpointExists) {
        return;
      }
      const obj = runtime[lineKey][timeKey];
      // local key特殊处理
      if (obj.local && breakpointExists.expandedVariables.local) {
        Object.keys(obj.local).forEach((localName) => {
          Object.keys(breakpointExists.expandedVariables.local).forEach((localKey) => {
            const localExists = breakpointExists.expandedVariables.local[localKey];
            if (localExists['@name'] === localName) {
              obj.local[localKey] = obj.local[localName];
            }
          });
          delete obj.local[localName];
        });
      } else {
        delete obj.local;
      }
      const resultValues = tests.getTestCaseValues(obj, '@value', 'expandedVariables');
      const doc = { $set: {}, $unset: {} };
      Object.keys(resultValues).forEach((field) => {
        const fieldPath = field.slice(0, -'value'.length);
        doc.$set[`${fieldPath}result`] = resultValues[field];
        const expectResults = tests.getTestCaseValues(breakpointExists.expandedVariables, '@result', 'expandedVariables');
        Object.keys(expectResults).forEach((field) => {
          if (!resultValues[`${field.slice(0, -'result'.length)}value`]) {
            doc.$unset[field] = 1;
          }
        });
        ps.push(BreakPoint.updateOne({ _id: breakpointExists._id }, doc));
      });
    });
  });
  return Promise.all(ps);
}
/**
 * 更新断点变量值
 * @param {String|ObjectId} breakPointId 断点id
 * @param {Object} breakPoint 断点
 * @param {String} key 变量path
 * @param {String} value 变量值
 * @returns
 */
async function updateBreakPointVaraibleValue(breakPointId, breakPoint, key, value) {
  if (!breakPoint) {
    breakPoint = await BreakPoint.findById(breakPointId);
  }
  const keyBaseFieldPath = key.substring(0, key.lastIndexOf('@value') - 1);
  const doc = {
    $set: {
      [`expandedVariables.${key}`]: value,
    },
    $unset: {
      [`expandedVariables.${keyBaseFieldPath}.@result`]: 1,
    },
  };
  await BreakPoint.updateOne({ _id: breakPointId }, doc);
  await tests.removeResultOfExecution(breakPoint.testcaseId);
  return functions.updateFunctionCoverage(breakPoint.functionId);
  // todo update invoke coverage
}
function getBreakPointsByVersionId(versionId) {
  const variableKeysObj = {};
  variableKeys.forEach((key) => {
    variableKeysObj[key] = 1;
  });
  return BreakPoint.aggregate([
    {
      $match: { versionId: mongoose.Types.ObjectId(versionId) },
    },
    {
      $lookup: {
        from: 'functions',
        localField: 'functionId',
        foreignField: '_id',
        as: 'breakpointFunc',
      },
    },
    {
      $unwind: '$breakpointFunc',
    },
    {
      $lookup: {
        from: 'files',
        localField: 'fileId',
        foreignField: '_id',
        as: 'breakpointFile',
      },
    },
    {
      $unwind: '$breakpointFile',
    },
    {
      $project: {
        line: 1,
        times: 1,
        exit: 1,
        code: 1,
        testcaseId: 1,
        fileId: 1,
        filePath: '$breakpointFile.path',
        functionId: 1,
        functionName: '$breakpointFunc.functionName',
        mangledId: '$breakpointFunc.mangledId',
        entryFunctionId: 1,
        ...variableKeysObj,
      },
    },
  ]);
}
module.exports = {
  addBreakPointPointTargets,
  deleteBreakPoint,
  getBreakPoint,
  getBreakPointsByTestcase,
  getJson,
  getObservableVariables,
  getBreakPointsByVersionId,

  recurseGenerate,
  upsertBreakPoint,
  updateBreakPointArrayIndexRange,
  updateBreakPointOutputResultByTestcase,
  updateBreakPointVaraibleValue,
};
exports = Object.assign(exports, module.exports);
